//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.eResearch.Execution
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using System.Text;
    using System.Xml;
    using Microsoft.Research.ScientificWorkflow.TridentUtilities;
    using Microsoft.Research.eResearch.RuntimeServices;
    using TridentContext;
    using Wf = System.Workflow.ComponentModel;
    using Microsoft.Research.eResearch.RuntimeServices.Common;
    using Microsoft.Research.eResearch.Common;
    using System.Globalization;
    using TridentAPI;
    using Microsoft.Research.DataLayer;

    /// <summary>
    /// Specialized dervided class that executes the new workflows.
    /// </summary>
    internal class NewWorkflowExecutor : WorkflowExecutor
    {
        /// <summary>
        /// The connection object.
        /// </summary>
        private DataLayer.Connection conn;

        /// <summary>
        /// The job id.
        /// </summary>
        private Guid jobId;

        private Job job;

        /// <summary>
        /// Used to maintain the QualifiedNames of the different iterators in the
        /// Workflow.
        /// </summary>
        private Stack<string> iteratorStack = new Stack<string>();

        /// <summary>
        /// Initializes a new instance of the NewWorkflowExecutor class.
        /// </summary>
        /// <param name="jobId">Registry job id to be executed.</param>
        /// <param name="loadedAssemblies">Loaded assemblies collection.</param>
        /// <param name="workflowName">Name of the workflow.</param>
        /// <param name="xomlPath">Path of the workflow XOML.</param>
        internal NewWorkflowExecutor(Guid jobId, Collection<Assembly> loadedAssemblies, string workflowName, string xomlPath, DataLayer.Connection conn, bool isLightWeightExecution)
            : base(jobId, loadedAssemblies, workflowName, isLightWeightExecution)
        {
            this.conn = conn;
            this.jobId = jobId;
            this.job = DataLayer.Job.Load(this.jobId, this.conn);
            NewWorkflowExecutor.AddSetterActivity(xomlPath);
            using (StreamReader streamReader = new StreamReader(xomlPath))
            {
                using (XmlTextReader reader = new XmlTextReader(streamReader.BaseStream))
                {
                    Instance = Runtime.CreateWorkflow(reader);
                }
            }
        }

        /// <summary>
        /// This method modifies the WF xoml code to insert the special setter
        /// activity for instantiating the type providers associated with the
        /// activity instance.
        /// </summary>
        private static void AddSetterActivity(string xomlPath)
        {
            ActivityInstance actInstance = TridentContext.Context.Get().WFInstance;
            if (null == actInstance)
                return;

            XmlDocument wfXml = new XmlDocument();
            wfXml.Load(xomlPath);

            if (null != wfXml.FirstChild.Attributes.GetNamedItem("xmlns:exec"))
                return;

            XmlAttribute setterNs = wfXml.CreateAttribute("xmlns:exec");
            setterNs.Value = "http://schemas.com/Microsoft.Research.eResearch.Execution";
            wfXml.FirstChild.Attributes.Append(setterNs);

            // Create Xml node for the setter activity. The string represenation for
            // the node is "<exec:Setter x:Name=\"TypeInitializerSetter0\"/>"
            XmlNode setterNode = wfXml.CreateNode(XmlNodeType.Element, "exec:Setter",
                "http://schemas.com/Microsoft.Research.eResearch.Execution");
            XmlAttribute setterName = wfXml.CreateAttribute("x:Name");
            setterName.Value = "TypeInitializerSetter0";

            setterNode.Attributes.Append(setterName);
            wfXml.FirstChild.PrependChild(setterNode);
            wfXml.Save(xomlPath);
        }

        /// <summary>
        /// Execute the workflow instance.
        /// </summary>
        public override void Execute()
        {
            ThrowIfDisposed();
            if (0 == this.PopulateCustomTypeHash())
            {
                this.RemoveSetter();
            }

            this.PopulateWorkflowOutputHash();

            if (job.IsDebug)
            {
                // When running in debug mode, give a chance to the debugger to attach
                if (!Debugger.IsAttached)
                    Debugger.Launch();
                else
                    Debugger.Break();
            }

            IsStarted = true;
            Instance.Start();
        }

        /// <summary>
        /// Remove the setter as there are no custom types' values to be set.
        /// </summary>
        private void RemoveSetter()
        {
            Wf.Activity rootActivity = Instance.GetWorkflowDefinition();
            if (rootActivity != null)
            {
                try
                {
                    Wf.WorkflowChanges wfChanges = new Wf.WorkflowChanges(rootActivity);
                    if (wfChanges != null && wfChanges.TransientWorkflow != null)
                    {
                        Wf.CompositeActivity rootTransientActivity = wfChanges.TransientWorkflow;
                        if (rootTransientActivity != null && rootTransientActivity.Activities.Count > 0
                            && rootTransientActivity.Activities[0].Name.Equals("TypeInitializerSetter0"))
                        {
                            rootTransientActivity.Activities.RemoveAt(0);
                            Instance.ApplyWorkflowChanges(wfChanges);
                        }
                    }
                }
                catch (InvalidOperationException) { }
                catch (Exception) { }
            }
        }

        /// <summary>
        /// Check if the workflow has any custom type initializers associated with it.
        /// If yes, run through all the activity sequences of the workflow, and for each of them,
        /// get the object values via the type initializer, add it to a hashtable. This hashtable is part 
        /// of the workflow's runtime service. It will be accessed during execution and the Setter activity wil
        /// assign the appropriate values to each of the activities.
        /// </summary>
        private int PopulateCustomTypeHash()
        {
            int counter = 0;
            if (!(Guid.Empty.Equals(this.jobId)) && this.conn != null)
            {
                ExceptionHandler.Handle(
                    PolicyName.LoggingPolicy,
                    () =>
                    {
                        if (job != null && job.ActivityInstances.Count > 0
                            && job.ActivityInstances[0].Activity != null && job.ActivityInstances[0].Activity.BaseSequence != null)
                        {
                            JobExecutionService jobExecSvc = Instance.WorkflowRuntime.GetService(typeof(JobExecutionService)) as JobExecutionService;
                            if (jobExecSvc != null && jobExecSvc.ParamsHash != null)
                            {
                                DataLayer.ActivitySequence baseSeq = job.ActivityInstances[0].Activity.BaseSequence;
                                NewWorkflowExecutor.AddToParamsHash(jobExecSvc.ParamsHash, baseSeq, job.ActivityInstances[0].ActivityInstanceParameters);
                                counter = jobExecSvc.ParamsHash.Count;
                            }
                        }
                    });
            }

            return counter;
        }

        /// <summary>
        /// For the passed activity sequence, assign values for custom types, if any. Do this recursively for all the child activities.
        /// </summary>
        /// <param name="paramsHash">The hashtable with the key of activityUniqueName_paramName.</param>
        /// <param name="actSequence">The activity sequence, the values of which need to be updated.</param>
        /// <param name="jobParams">The job required parameters.</param>
        private static void AddToParamsHash(Hashtable paramsHash, DataLayer.ActivitySequence actSequence, ActivityInstanceParameterCollection<ActivityInstance> jobParams)
        {
            string currentValue = string.Empty;

            if (actSequence != null)
            {
                foreach (ParameterAssignment paramAssign in actSequence.ParameterAssignments)
                {

                    if (paramAssign.ActivityParameter.Direction == DirectionType.Input && !paramAssign.IsBound)
                    {
                        currentValue = GetUpdatedValue(paramAssign, jobParams);
                        try
                        {
                            object actualVal = null;
                            TypeInitializer typeInit = TypeProviderHelper.FetchTypeInitializer(paramAssign.ActivityParameter.Type, DataLayer.Runtime.WPF);
                            if (typeInit != null)
                            {
                                BaseTypeInitializer baseTypeInit = TypeProviderHelper.LoadInitializer(typeInit, currentValue);
                                if (baseTypeInit != null)
                                {
                                    actualVal = baseTypeInit.GetValue();
                                }
                            }
                            else
                            {
                                actualVal = TypeProviderHelper.FetchDeserializedValue(
                                    AssemblyLoad.SearchTypeIncurrentDomain(paramAssign.ActivityParameter.Type),
                                    currentValue);
                            }
                            if (actualVal != null)
                            {
                                paramsHash[WorkflowRuntimeHelper.FetchActivityParamUniqueKey(actSequence.Name, paramAssign.ActivityParameter.Name)] = actualVal;
                            }
                        }
                        catch (Exception) { }
                    }
                }

                foreach (DataLayer.ActivitySequence childSeq in actSequence.Children)
                {
                    NewWorkflowExecutor.AddToParamsHash(paramsHash, childSeq, jobParams);
                }
            }
        }

        /// <summary>
        /// Populates all the workflow output parameters in a hash table. This hashtable will be used by the WorkflowOutput tracking
        /// service to determine whether the output is a workflow required output.
        /// </summary>        
        private void PopulateWorkflowOutputHash()
        {
            int counter = 0;
            if (!(Guid.Empty.Equals(this.jobId)) && this.conn != null)
            {
                ExceptionHandler.Handle(
                   PolicyName.LoggingPolicy,
                   () =>
                   {
                       if (job != null && job.ActivityInstances.Count > 0
                           && job.ActivityInstances[0].Activity != null && job.ActivityInstances[0].Activity.BaseSequence != null)
                       {
                           JobExecutionService jobExecSvc = Instance.WorkflowRuntime.GetJobExecutionService();
                           if (jobExecSvc != null && jobExecSvc.OutputParamsHash != null)
                           {
                               DataLayer.ActivitySequence baseSeq = job.ActivityInstances[0].Activity.BaseSequence;
                               AddToOutputParamsHash(jobExecSvc.OutputParamsHash, baseSeq);
                               counter = jobExecSvc.OutputParamsHash.Count;
                           }
                       }
                   });
            }
        }

        /// <summary>
        /// For the passed activity sequence, add the workflow required output parameters to the hash table, if any. 
        /// Do this recursively for all the child activities.
        /// </summary>
        /// <param name="outputParamsHash">The hashtable with the key of activityUniqueName_paramName.</param>
        /// <param name="actSequence">The activity sequence.</param>
        private void AddToOutputParamsHash(Hashtable outputParamsHash, DataLayer.ActivitySequence actSequence)
        {
            if (actSequence != null)
            {
                if (WorkflowRuntimeHelper.IsIterator(actSequence))
                {
                    iteratorStack.Push(actSequence.Name);
                }

                foreach (ParameterAssignment paramAssign in actSequence.ParameterAssignments)
                {
                    ExceptionHandler.Handle(
                    PolicyName.LoggingPolicy,
                    () =>
                    {
                        if (paramAssign.RequiredForWorkflow && paramAssign.ActivityParameter.Direction == DirectionType.Output)
                        {
                            string paramkey = WorkflowRuntimeHelper.FetchActivityParamUniqueKey(actSequence.Name, paramAssign.ActivityParameter.Name);
                            outputParamsHash[paramkey] = paramAssign;
                            if (iteratorStack.Count != 0 && paramAssign.OutputLabel.ToUpper(CultureInfo.InvariantCulture).Contains("%ITERATOR%"))
                            {
                                string iteratorKey = WorkflowRuntimeHelper.FetchActivityIteratorUniqueKey(paramkey);
                                outputParamsHash[iteratorKey] = iteratorStack.Peek();
                            }
                        }
                    });
                }

                foreach (DataLayer.ActivitySequence childSeq in actSequence.Children)
                {
                    // check if the previous sibling was an iterator
                    if (childSeq.Position >= 1 && WorkflowRuntimeHelper.IsIterator(actSequence.Children[childSeq.Position - 1]))
                    {
                        if (iteratorStack.Count != 0)
                        {
                            iteratorStack.Pop();
                        }
                    }

                    AddToOutputParamsHash(outputParamsHash, childSeq);

                    // check if this is the last child
                    if (childSeq.Parent != null)
                    {
                        if ((childSeq.Parent.Children.Count == childSeq.Position + 1) && (WorkflowRuntimeHelper.IsIterator(childSeq)))
                        {
                            if (iteratorStack.Count != 0)
                            {
                                iteratorStack.Pop();
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets the latest value of the parameter.
        /// </summary>
        /// <param name="paramAssign">The param assign.</param>
        /// <param name="jobParams">The job required parameters.</param>
        /// <returns>Latest value of the parameter.</returns>
        private static string GetUpdatedValue(ParameterAssignment paramAssign, ActivityInstanceParameterCollection<ActivityInstance> jobParams)
        {
            ActivityInstanceParameter currentInstance = null;
            foreach (ActivityInstanceParameter jobParam in jobParams)
            {
                if (jobParam.ParameterAssignment.Equals(paramAssign))
                {
                    currentInstance = jobParam;
                    break;
                }
            }

            if (currentInstance == null)
            {
                return paramAssign.CurrentValue;
            }
            else
            {
                return currentInstance.ParamValue;
            }
        }
    }
}
